package com.swinw.asumall.product.service.impl;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.TypeReference;
import com.swinw.asumall.product.service.CategoryBrandRelationService;
import com.swinw.asumall.product.vo.Catalog2LaveVo;
import com.swinw.asumall.product.vo.CatalogMapVo;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Service;

import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.swinw.common.utils.PageUtils;
import com.swinw.common.utils.Query;

import com.swinw.asumall.product.dao.CategoryDao;
import com.swinw.asumall.product.entity.CategoryEntity;
import com.swinw.asumall.product.service.CategoryService;
import org.springframework.transaction.annotation.Transactional;


@Service("categoryService")
public class CategoryServiceImpl extends ServiceImpl<CategoryDao, CategoryEntity> implements CategoryService {

    @Autowired
    CategoryBrandRelationService categoryBrandRelationService;

    @Autowired
    StringRedisTemplate stringRedisTemplate; //redis缓存

    @Override
    public PageUtils queryPage(Map<String, Object> params) {
        IPage<CategoryEntity> page = this.page(
                new Query<CategoryEntity>().getPage(params),
                new QueryWrapper<CategoryEntity>()
        );

        return new PageUtils(page);
    }

    /**
     * 查询三级分类
     *
     * @return
     */
    @Cacheable(value = "catalog",key = "#root.methodName",sync = true)
    @Override
    public List<CategoryEntity> listTree() {
        //查询所有的菜单，包括一级菜单、二级菜单、三级菜单
        List<CategoryEntity> categoryEntities = baseMapper.selectList(null);

        List<CategoryEntity> listTree = categoryEntities.stream().filter(categoryEntity -> {
            return categoryEntity.getParentCid() == 0; //过滤一级菜单
        }).map(categoryEntity -> {//为每个一级菜单封装它的子级菜单
            categoryEntity.setChildren(childCategoryList(categoryEntity, categoryEntities));
            return categoryEntity;
        }).sorted((elem1, elem2) -> {//对当前的菜单进行排序
            return (elem1.getSort() == null ? 0 : elem1.getSort()) - (elem2.getSort() == null ? 0 : elem2.getSort());
        }).collect(Collectors.toList());//将结果收集到集合中

        return listTree;
    }

    @CacheEvict(value = "catalog",allEntries = true)
    @Override
    public void removeMenusByIds(List<Long> asList) {
        //TODO 没有被其他资源关联的分类才能被删除
        baseMapper.deleteBatchIds(asList);
    }

    @Override
    public Long[] findCatalogPath(Long catelogId) {
        ArrayList<Long> paths = new ArrayList<>();
        CategoryEntity categoryEntity = baseMapper.selectById(catelogId);
        dfs(paths, categoryEntity);
        Collections.reverse(paths);
        return paths.toArray(new Long[paths.size()]);
    }

    @CacheEvict(value = "catalog",allEntries = true)
    @Transactional
    @Override
    public void updateCascade(CategoryEntity category) {
        this.updateById(category);
        categoryBrandRelationService.updateCascad(category.getCatId(), category.getName());
    }

    /**
     * 通过分类Id查询分类信息
     *
     * @param catalogId
     * @return
     */
    @Override
    public CategoryEntity selectById(Long catalogId) {
        return this.baseMapper.selectById(catalogId);
    }

    /**
     * 查询一级分类信息
     *
     * @return
     */
    @Cacheable(value = "catalog",key = "#root.methodName")
    @Override
    public List<CategoryEntity> catelogLaveOne(Long catId) {
        QueryWrapper<CategoryEntity> queryWrapper = new QueryWrapper<>();
        queryWrapper.eq("parent_cid", catId);
        List<CategoryEntity> categoryEntities = this.baseMapper.selectList(queryWrapper);
        return categoryEntities;
    }

    /**
     * 查询二级分类及二级分类下的三级分类
     *  TODO 堆外内存溢出 OutOfDireceMemoryError
     *  1、springBoot2.0以后默认使用Lettuce作为操作Redis的客户端
     *      1.1、使用Netty进行网络通信（堆外溢出的原因）
     *      如果在程序运行的过程中没有指定堆外内存，默认就会使用最大内存（默认实际内存的25% -Xmx1024m 设置最大运行内存）
     *     <dependency>
     *       <groupId>io.lettuce</groupId>
     *       <artifactId>lettuce-core</artifactId>
     *       <version>5.2.1.RELEASE</version>
     *       <scope>compile</scope>
     *     </dependency>
     *     1.2、可以通过 -Dio.netty.MaxDirectMemory 调大堆外内存（可以暂缓异常出现）
     *  2、解决
     *      2.1、升级Lettuce客户端
     *      2.2、切换操作redis的客户端（jedis）
     *          <dependency>
     *             <groupId>org.springframework.boot</groupId>
     *             <artifactId>spring-boot-starter-data-redis</artifactId>
     *             <!-- 排除lettuce -->
     *             <exclusions>
     *                 <exclusion>
     *                     <groupId>io.lettuce</groupId>
     *                     <artifactId>lettuce-core</artifactId>
     *                 </exclusion>
     *             </exclusions>
     *         </dependency>
     *         <dependency>
     *             <groupId>redis.clients</groupId>
     *             <artifactId>jedis</artifactId>
     *         </dependency>
     *
     *   3、补充
     *      lettuce、jedis都是操作Redis的底层客户端，Springboot再次封装StringRedisTemplate
     *
     *   4、缓存带来的问题
     *      4.1、缓存穿透（访问缓存中不存在的数据）
     *          解决：给不存在的数据存入null，设置短暂的有效时间
     *
     *      4.2、缓存雪崩（缓存中的大量数据在同一时刻失效）
     *          解决：在现有的有效时间基础上添加随机过期时间
     *
     *      4.3、缓存击穿（热点数据）
     *          解决：加锁（当拿到锁的人去查询数据库，下一个拿到锁的人就不会查询数据库，而是直接从缓存中获取数据）
     * @return
     */
    @Cacheable(value = "catalog",key = "#root.method.name",sync = true)
    @Override
    public Map<String, List<Catalog2LaveVo>> getCatalogMap(){
        List<CategoryEntity> categoryEntityList = baseMapper.selectList(null);
        //1、获得一级分类
        List<CategoryEntity> categoryEntities = catelogLaveByCatId(categoryEntityList, 0L);
        //2、查询二级分类
        Map<String, List<Catalog2LaveVo>> stringListMap = categoryEntities.stream().map(item -> {
            CatalogMapVo catalogMapVo = new CatalogMapVo();
            //2.1、查询当前一级分类下的所有二级分类
            List<CategoryEntity> lave2Entities = catelogLaveByCatId(categoryEntityList, item.getCatId());

            //3、查询三级分类
            List<Catalog2LaveVo> catalog2LaveVos = lave2Entities.stream().map(elem -> {

                Catalog2LaveVo catalog2LaveVo1 = new Catalog2LaveVo();
                //3.1、查询当前二级分类下的所有三级分类
                List<CategoryEntity> lave3Entities = catelogLaveByCatId(categoryEntityList, elem.getCatId());
                //3、封装三级分类信息
                List<Catalog2LaveVo.Catalog3LaveVo> collect = lave3Entities.stream().map(e -> {
                    Catalog2LaveVo.Catalog3LaveVo catalog3LaveVo = new Catalog2LaveVo.Catalog3LaveVo();
                    catalog3LaveVo.setCatalog2Id(elem.getCatId().toString());
                    catalog3LaveVo.setId(e.getCatId().toString());
                    catalog3LaveVo.setName(e.getName());
                    return catalog3LaveVo;
                }).collect(Collectors.toList());
                //2.2、封装二级分类数据
                catalog2LaveVo1.setCatalog1Id(item.getCatId().toString());
                catalog2LaveVo1.setId(elem.getCatId().toString());
                catalog2LaveVo1.setName(elem.getName());
                catalog2LaveVo1.setCatalog3List(collect);
                return catalog2LaveVo1;
            }).collect(Collectors.toList());

            //4、转换数据结构
            catalogMapVo.setKey(item.getCatId().toString());
            catalogMapVo.setList(catalog2LaveVos);
            return catalogMapVo;
        }).collect(Collectors.toMap(k -> k.getKey(), v -> v.getList()));
        return stringListMap;
    }

    /**
     * 解决缓存击穿
     * 1、锁当前的对象，SpringBoot所有的组件在容器中都是单例的。this（当前对象）是单例，所以所有的请求都使用的是同一个对象
     *      （可以加在方法上） public synchronized Map<String, List<Catalog2LaveVo>> getCatalogJsonMap()
     *    synchronized (this){
     *        //1、得到锁再去缓存中确定缓存中是否存在数据  199-202
     *        //2、业务代码 203-238
     *        //3、存入缓存，返回结果 240-244
     *        //4、释放锁
     *    }
     *    优点：较快
     *    缺点：本地锁，只能锁住当前的进程，在分布式环境下，如果每一个节点都要添加一把锁，不同节点都查询相同的数据
     *      不能满足我们的需求
     * TODO 本地锁 synchronized、JUC(Lock)
     *
     * @return
     */
    public Map<String, List<Catalog2LaveVo>> getCatalogJsonMap() {
        synchronized (this){
            String catalogJson = stringRedisTemplate.opsForValue().get("catalogJsonMap");
            if (!StringUtils.isEmpty(catalogJson)) { //缓存中存在数据
                return JSON.parseObject(catalogJson, new TypeReference<Map<String, List<Catalog2LaveVo>>>() {
                });//将查询的数据转为我们需要的对象
            }
            List<CategoryEntity> categoryEntityList = baseMapper.selectList(null);
            //1、获得一级分类
            List<CategoryEntity> categoryEntities = catelogLaveByCatId(categoryEntityList, 0L);
            //2、查询二级分类
            Map<String, List<Catalog2LaveVo>> stringListMap = categoryEntities.stream().map(item -> {
                CatalogMapVo catalogMapVo = new CatalogMapVo();
                //2.1、查询当前一级分类下的所有二级分类
                List<CategoryEntity> lave2Entities = catelogLaveByCatId(categoryEntityList, item.getCatId());

                //3、查询三级分类
                List<Catalog2LaveVo> catalog2LaveVos = lave2Entities.stream().map(elem -> {

                    Catalog2LaveVo catalog2LaveVo1 = new Catalog2LaveVo();
                    //3.1、查询当前二级分类下的所有三级分类
                    List<CategoryEntity> lave3Entities = catelogLaveByCatId(categoryEntityList, elem.getCatId());
                    //3、封装三级分类信息
                    List<Catalog2LaveVo.Catalog3LaveVo> collect = lave3Entities.stream().map(e -> {
                        Catalog2LaveVo.Catalog3LaveVo catalog3LaveVo = new Catalog2LaveVo.Catalog3LaveVo();
                        catalog3LaveVo.setCatalog2Id(elem.getCatId().toString());
                        catalog3LaveVo.setId(e.getCatId().toString());
                        catalog3LaveVo.setName(e.getName());
                        return catalog3LaveVo;
                    }).collect(Collectors.toList());
                    //2.2、封装二级分类数据
                    catalog2LaveVo1.setCatalog1Id(item.getCatId().toString());
                    catalog2LaveVo1.setId(elem.getCatId().toString());
                    catalog2LaveVo1.setName(elem.getName());
                    catalog2LaveVo1.setCatalog3List(collect);
                    return catalog2LaveVo1;
                }).collect(Collectors.toList());

                //4、转换数据结构
                catalogMapVo.setKey(item.getCatId().toString());
                catalogMapVo.setList(catalog2LaveVos);
                return catalogMapVo;
            }).collect(Collectors.toMap(k -> k.getKey(), v -> v.getList()));

            //1、将数据转为字符串
            String toJSONString = JSON.toJSONString(stringListMap);
            //2、将查询的数据放入缓存,缓存时间为一天
            stringRedisTemplate.opsForValue().set("catalogJsonMap", toJSONString, 1, TimeUnit.DAYS);
            return stringListMap;
        }
    }

    /**
     * 分布式锁
     *  1、去Redis中抢占锁
     *
     *  问题：如果程序在执行的过程中遇到了异常，或者是服务宕机，没有执行删除锁的代码，那么就会出现死锁
     *  解决：设置锁的自动过期时间，即使没有删除锁，时间到了，也会自动删除锁
     *
     *  2、分布式锁使用规则
     *      2.1、原子加锁
     *      2.2、原子解锁
     * @return
     */
    public Map<String, List<Catalog2LaveVo>> getCatalogJsonMapRedisLock() {
        //抢坑,设置锁有效时间(指定30秒后锁过期,使用原子命令)
        String uuid = UUID.randomUUID().toString();
        Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent("lock", uuid,300,TimeUnit.SECONDS);
        if(lock){ //占到坑位了
            //1、执行业务代码
            System.out.println("加锁成功，执行业务代码++++++++++++++++++++");
            Map<String, List<Catalog2LaveVo>> dataFromDb;
            try{
                dataFromDb = getDataFromDb();
            }finally {
                //2、原子性删锁（lua脚本）,如果返回1表示删锁成功，返回0表示不成功
                String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
                stringRedisTemplate.execute(new DefaultRedisScript<Long>(script,Long.class),Arrays.asList("lock"),uuid);
            }
            //2、非原子解锁,确保是自己的锁，如果不是自己的锁，不能删除
            //String lockValue = stringRedisTemplate.opsForValue().get("lock");
            //if(lockValue.equals(uuid)){
            //   stringRedisTemplate.delete("lock");
            //}
            return dataFromDb;
        }else{
            //1、加锁失败，重试
            return getCatalogJsonMapRedisLock();
        }
    }

    private Map<String, List<Catalog2LaveVo>> getDataFromDb() {
        String catalogJson = stringRedisTemplate.opsForValue().get("catalogJsonMap");
        if (!StringUtils.isEmpty(catalogJson)) { //缓存中存在数据
            return JSON.parseObject(catalogJson, new TypeReference<Map<String, List<Catalog2LaveVo>>>() {
            });//将查询的数据转为我们需要的对象
        }
        System.out.println("缓存中不存在数据，查询数据库数据+++++++++++++++");
        List<CategoryEntity> categoryEntityList = baseMapper.selectList(null);
        //1、获得一级分类
        List<CategoryEntity> categoryEntities = catelogLaveByCatId(categoryEntityList, 0L);
        //2、查询二级分类
        Map<String, List<Catalog2LaveVo>> stringListMap = categoryEntities.stream().map(item -> {
            CatalogMapVo catalogMapVo = new CatalogMapVo();
            //2.1、查询当前一级分类下的所有二级分类
            List<CategoryEntity> lave2Entities = catelogLaveByCatId(categoryEntityList, item.getCatId());

            //3、查询三级分类
            List<Catalog2LaveVo> catalog2LaveVos = lave2Entities.stream().map(elem -> {

                Catalog2LaveVo catalog2LaveVo1 = new Catalog2LaveVo();
                //3.1、查询当前二级分类下的所有三级分类
                List<CategoryEntity> lave3Entities = catelogLaveByCatId(categoryEntityList, elem.getCatId());
                //3、封装三级分类信息
                List<Catalog2LaveVo.Catalog3LaveVo> collect = lave3Entities.stream().map(e -> {
                    Catalog2LaveVo.Catalog3LaveVo catalog3LaveVo = new Catalog2LaveVo.Catalog3LaveVo();
                    catalog3LaveVo.setCatalog2Id(elem.getCatId().toString());
                    catalog3LaveVo.setId(e.getCatId().toString());
                    catalog3LaveVo.setName(e.getName());
                    return catalog3LaveVo;
                }).collect(Collectors.toList());
                //2.2、封装二级分类数据
                catalog2LaveVo1.setCatalog1Id(item.getCatId().toString());
                catalog2LaveVo1.setId(elem.getCatId().toString());
                catalog2LaveVo1.setName(elem.getName());
                catalog2LaveVo1.setCatalog3List(collect);
                return catalog2LaveVo1;
            }).collect(Collectors.toList());

            //4、转换数据结构
            catalogMapVo.setKey(item.getCatId().toString());
            catalogMapVo.setList(catalog2LaveVos);
            return catalogMapVo;
        }).collect(Collectors.toMap(k -> k.getKey(), v -> v.getList()));

        //1、将数据转为字符串
        String toJSONString = JSON.toJSONString(stringListMap);
        //2、将查询的数据放入缓存,缓存时间为一天
        stringRedisTemplate.opsForValue().set("catalogJsonMap", toJSONString, 1, TimeUnit.DAYS);
        return stringListMap;
    }

    private List<CategoryEntity> catelogLaveByCatId(List<CategoryEntity> categoryEntityList, long catId) {
        return categoryEntityList.stream().filter(item -> {
            return item.getParentCid() == catId;
        }).collect(Collectors.toList());
    }

    private void dfs(ArrayList<Long> paths, CategoryEntity categoryEntity) {
        Long catId = categoryEntity.getCatId();
        paths.add(catId);
        if (categoryEntity.getParentCid() != 0) {
            dfs(paths, baseMapper.selectById(categoryEntity.getParentCid()));
        }
    }

    private List<CategoryEntity> childCategoryList(CategoryEntity parent, List<CategoryEntity> categoryEntities) {
        List<CategoryEntity> childList = categoryEntities.stream().filter(elem -> {
            return elem.getParentCid() == parent.getCatId();//过滤出当前菜单的子菜单
        }).map(elem -> {//递归封装当前菜单的子菜单
            elem.setChildren(childCategoryList(elem, categoryEntities));
            return elem;
        }).sorted((elem1, elem2) -> {//对当前菜单进行排序
            return (elem1.getSort() == null ? 0 : elem1.getSort()) - (elem2.getSort() == null ? 0 : elem2.getSort());
        }).collect(Collectors.toList());//将当前的结果收集到集合中
        return childList;
    }

}